#include "viewer.hpp"
#include <iostream>
#include <GL/gl.h>
#include <GL/glu.h>
#include "draw.hpp"

Viewer::Viewer()
{
  Glib::RefPtr<Gdk::GL::Config> glconfig;

  // Ask for an OpenGL Setup with
  //  - red, green and blue component colour
  //  - a depth buffer to avoid things overlapping wrongly
  //  - double-buffered rendering to avoid tearing/flickering
  glconfig = Gdk::GL::Config::create(Gdk::GL::MODE_RGB |
                                     Gdk::GL::MODE_DEPTH |
                                     Gdk::GL::MODE_DOUBLE);
  if (glconfig == 0) {
    // If we can't get this configuration, die
    std::cerr << "Unable to setup OpenGL Configuration!" << std::endl;
    abort();
  }

  // Accept the configuration
  set_gl_capability(glconfig);

  // Register the fact that we want to receive these events
  add_events(Gdk::BUTTON1_MOTION_MASK    |
             Gdk::BUTTON2_MOTION_MASK    |
             Gdk::BUTTON3_MOTION_MASK    |
             Gdk::BUTTON_PRESS_MASK      | 
             Gdk::BUTTON_RELEASE_MASK    |
	     Gdk::POINTER_MOTION_MASK	 |
             Gdk::VISIBILITY_NOTIFY_MASK);
}

Viewer::~Viewer()
{
  // Nothing to do here right now.
}

void Viewer::invalidate()
{
  // Force a rerender
  Gtk::Allocation allocation = get_allocation();
  get_window()->invalidate_rect( allocation, false);
}

void Viewer::set_perspective(double fov, double aspect,
                             double near, double far)
{
  // Fill me in!
	double rad = fov/180*M_PI;
	Vector4D r1 = Vector4D( 1/(tan(rad/2)*aspect), 0, 0 ,0 );
	Vector4D r2 = Vector4D( 0, 1/tan(rad/2), 0, 0 );
	Vector4D r3 = Vector4D( 0, 0, (far+near)/(far-near), -2*near*far/(far-near) );
	Vector4D r4 = Vector4D( 0, 0, 1, 0 );
	m_projection = Matrix4x4(r1,r2,r3,r4);	
}

void Viewer::reset_view()
{
  // Fill me in!
	initialize();
	invalidate();
	setLabelContext();
}

void Viewer::on_realize()
{
  // Do some OpenGL setup.
  // First, let the base class do whatever it needs to
  Gtk::GL::DrawingArea::on_realize();
  
  Glib::RefPtr<Gdk::GL::Drawable> gldrawable = get_gl_drawable();
  
  if (!gldrawable)
    return;

  if (!gldrawable->gl_begin(get_gl_context()))
    return;

  gldrawable->gl_end();
	// m_mode
	m_mode = MODELROTATE;
	reset_view();
}

void Viewer::initialize()
{
	// setup a cube
	m_cube[0] = Point3D(-1.0, -1.0, 1.0);
	m_cube[1] = Point3D(-1.0, 1.0, 1.0);
	m_cube[2] = Point3D(1.0, 1.0, 1.0);
	m_cube[3] = Point3D(1.0, -1.0, 1.0);
	m_cube[4] = Point3D(-1.0, -1.0, -1.0);
	m_cube[5] = Point3D(-1.0, 1.0, -1.0);
	m_cube[6] = Point3D(1.0, 1.0, -1.0);
	m_cube[7] = Point3D(1.0, -1.0, -1.0);
	// setup world gnomon
	m_world_gnomon[0] = Point3D(0.0, 0.0, 0.0);
	m_world_gnomon[1] = Point3D(0.5, 0.0, 0.0);
	m_world_gnomon[2] = Point3D(0.0, -0.5, 0.0);
	m_world_gnomon[3] = Point3D(0.0, 0.0, 0.5);
	// setup model gnomon
	m_model_gnomon[0] = Point3D(0.0, 0.0, 0.0);
	m_model_gnomon[1] = Point3D(0.5, 0.0, 0.0);
	m_model_gnomon[2] = Point3D(0.0, -0.5, 0.0);
	m_model_gnomon[3] = Point3D(0.0, 0.0, 0.5);
	// mouse button
	button1 = false;
	button2 = false;
	button3 = false;
	// near and far and field of view
	p_near = 2.0;
	p_far = 10.0;
	p_FOV = 30.0;
	// view screen
	viewport_width = get_width()*0.9;
	viewport_height = get_height()*0.9;
	// viewport's left buttom to be orign
	viewport_x = viewport_width*0.05;
	viewport_y = viewport_height*0.05;
	// setup a viewport coordinates
	setViewportCoordinates();
	set_perspective(p_FOV, 1.0, p_near, p_far);
	// initialize projection, view, and model matrix
	Vector4D r1 = Vector4D(1, 0, 0, 0);
	Vector4D r2 = Vector4D(0, 1, 0, 0);
	Vector4D r3 = Vector4D(0, 0, 1, 5.0);
	Vector4D r4 = Vector4D(0, 0, 0, 1);
	m_view= Matrix4x4(r1, r2, r3, r4);

	m_model_scale = Matrix4x4();
	m_model_transrot = Matrix4x4();
	// mode = rotate model
	static_cast<Gtk::CheckMenuItem*>(&(*mode_menu).items()[0])->set_active();
}

bool Viewer::on_expose_event(GdkEventExpose* event)
{
  Glib::RefPtr<Gdk::GL::Drawable> gldrawable = get_gl_drawable();

  if (!gldrawable) return false;

  if (!gldrawable->gl_begin(get_gl_context()))
    return false;

  // Here is where your drawing code should go. 

  draw_init(get_width(), get_height());
		
	// setup a viewport coordinates
	setViewportCoordinates();
	Point3D temp_cube[8];
	Point3D temp_world_gnomon[4];
	Point3D temp_model_gnomon[4];
	// modelview cooridinates
	for(int i=0; i<8; i++)
	{
		temp_cube[i] = ((m_view * m_model_transrot) * m_model_scale) * m_cube[i];
		if(i < 4){
			temp_model_gnomon[i] = (m_view * m_model_transrot) * m_model_gnomon[i];
			temp_world_gnomon[i] = m_view * m_world_gnomon[i];
		}
			
	}
	// draw cube
	drawCube(temp_cube);
	// draw model gnomon
	drawModelGnomon(temp_model_gnomon);
	// draw world gnomon
	drawWorldGnomon(temp_world_gnomon);
  	// draw viewport
	drawViewport();
	
  draw_complete();
            
  // Swap the contents of the front and back buffers so we see what we
  // just drew. This should only be done if double buffering is enabled.
  gldrawable->swap_buffers();

  gldrawable->gl_end();

  return true;
}

bool Viewer::on_configure_event(GdkEventConfigure* event)
{
  Glib::RefPtr<Gdk::GL::Drawable> gldrawable = get_gl_drawable();

  if (!gldrawable) return false;
  
  if (!gldrawable->gl_begin(get_gl_context()))
    return false;

  gldrawable->gl_end();

  return true;
}

void Viewer::set_mode(myMode m)
{
	m_mode = m;
	setLabelContext();
}

void Viewer::set_label(Gtk::Label *l)
{
	m_label = l;
	setLabelContext();
}

void Viewer::set_mode_menu(Gtk::Menu *m)
{
	mode_menu = m;
}

// private functions

bool Viewer::nearPlaneClipping(Point3D *p1, Point3D *p2)
{
	Point3D near_point(0.0, 0.0, p_near);
	Vector3D near_plane_normal(0.0, 0.0, 1.0);
	double wecA = (*p1-near_point).dot(near_plane_normal);
	double wecB = (*p2-near_point).dot(near_plane_normal);
	if(wecA < 0 && wecB <0)
 		return false;
	else if(wecA < 0)
		*p1 = *p1 + wecA / (wecA - wecB) * (*p2 - *p1);
	else if(wecB < 0)
		*p2 = *p1 + wecA / (wecA - wecB) * (*p2 - *p1);
	return true;
}

bool Viewer::otherPlanesClipping(Point3D *p1, Point3D *p2)
{
	Point3D points[] = {Point3D(0.0,0.0,1.0),Point3D(0.0,1.0,0.0),Point3D(0.0,-1.0,0.0),Point3D(-1.0,0.0,0.0),Point3D(1.0,0.0,0.0)};
	Vector3D normals[] = {Vector3D(0.0,0.0,-1.0),Vector3D(0.0,-1.0,0.0),Vector3D(0.0,1.0,0.0),Vector3D(1.0,0.0,0.0),Vector3D(-1.0,0.0,0.0)};
	double wecA, wecB;
	for(int i=0; i<5; i++)
	{
		wecA = (*p1-points[i]).dot(normals[i]);
		wecB = (*p2-points[i]).dot(normals[i]);
		if(wecA < 0 && wecB <0)
	 		return false;
		else if(wecA < 0)
			*p1 = *p1 + wecA / (wecA - wecB) * (*p2 - *p1);
		else if(wecB < 0)
			*p2 = *p1 + wecA / (wecA - wecB) * (*p2 - *p1);
	}
	return true;
}

void Viewer::drawLine(Point3D p1, Point3D p2)
{
	//near clipping
	bool result = nearPlaneClipping(&p1, &p2);
	if(!result) return;
	// perspective projection
	Point3D homo_p1 = m_projection * p1;
	Point3D homo_p2 = m_projection * p2;
	// homogenizing
	homo_p1[0] = homo_p1[0]/p1[2];
	homo_p1[1] = homo_p1[1]/p1[2];
	homo_p1[2] = homo_p1[2]/p1[2];
	homo_p2[0] = homo_p2[0]/p2[2];
	homo_p2[1] = homo_p2[1]/p2[2];
	homo_p2[2] = homo_p2[2]/p2[2];
	// other clipping
	result = otherPlanesClipping(&homo_p1, &homo_p2);
	if(!result) return;
	// window to viewport mapping
	Point2D temp_p1 = mapViewport(Point2D(homo_p1[0], homo_p1[1]));
	Point2D temp_p2 = mapViewport(Point2D(homo_p2[0], homo_p2[1]));
	// viewport clipping
	
	// draw line
	draw_line( temp_p1, temp_p2 );
}

void Viewer::drawWorldGnomon(Point3D *p)
{
	set_colour(Colour(1.0, 0.0, 0.0));
	drawLine( p[0], p[1] );
	set_colour(Colour(0.0, 1.0, 0.0));
	drawLine( p[0], p[2] );
	set_colour(Colour(0.0, 0.0, 1.0));
	drawLine( p[0], p[3] );
}

void Viewer::drawModelGnomon(Point3D *p)
{
	set_colour(Colour(1.0, 1.0, 0.0));
	drawLine( p[0], p[1] );
	set_colour(Colour(0.0, 1.0, 1.0));
	drawLine( p[0], p[2] );
	set_colour(Colour(1.0, 0.0, 1.0));
	drawLine( p[0], p[3] );
}

void Viewer::drawCube(Point3D *p)
{
	set_colour(Colour(0.0, 0.0, 0.0));

	drawLine( p[0], p[1] );
	drawLine( p[1], p[2] );
	drawLine( p[2], p[3] );
	drawLine( p[3], p[0] );

	drawLine( p[4], p[5] );
	drawLine( p[5], p[6] );
	drawLine( p[6], p[7] );
	drawLine( p[7], p[4] );

	drawLine( p[0], p[4] );
	drawLine( p[1], p[5] );
	drawLine( p[2], p[6] );
	drawLine( p[3], p[7] );
}

Point2D Viewer::setNormalized(Point3D p)
{
	return Point2D(p[0]/p[2], p[1]/p[2]);
}

Point2D Viewer::mapViewport(Point2D p)
{
	// map [-1,1] to [0,1]
	double x = (p[0] + 1.0)/2;
	double y = (p[1] + 1.0)/2;
	// map to portview
	x = viewport_width * x + viewport_x;
	y = viewport_height * y + viewport_y;
	return Point2D(x,y);
}

void Viewer::drawViewport()
{
	set_colour(Colour(1.0, 0.0, 0.0));
	for(int i=0; i<4; i++){
		draw_line(m_viewport[i%4], m_viewport[(i+1)%4]);
	}
}

void Viewer::setViewportCoordinates()
{
	m_viewport[0] = Point2D(viewport_x,viewport_y);
	m_viewport[1] = Point2D(viewport_x + viewport_width,viewport_y);
	m_viewport[2] = Point2D(viewport_x + viewport_width,viewport_y + viewport_height);
	m_viewport[3] = Point2D(viewport_x,viewport_y + viewport_height);
}

void Viewer::setViewportAttributes(Point2D p1, Point2D p2)
{
	viewport_width = abs(p1[0] - p2[0]);
	viewport_height = abs(p1[1] - p2[1]);
	if(p1[0] < p2[0]){
		if(p1[1] < p2[1]){
			viewport_x = p1[0];
			viewport_y = p1[1];
		}
		else{
			viewport_x = p1[0];
			viewport_y = p1[1] - viewport_height;
		}
	}
	else{
		if(p1[1] < p2[1]){
			viewport_x = p1[0] - viewport_width;
			viewport_y = p1[1];
		}
		else{
			viewport_x = p2[0];
			viewport_y = p2[1];
		}
	}
	setViewportCoordinates();
}

void Viewer::setLabelContext()
{
	std::string mode;
	if(m_mode == MODELROTATE)
		mode = "MODEL ROTATE";
	else if(m_mode == MODELTRANSLATE)
		mode = "MODEL TRANSLATE";
	else if(m_mode == MODELSCALE)
		mode = "MODEL SCALE";
	else if(m_mode == VIEWROTATE)
		mode = "VIEW ROTATE";
	else if(m_mode == VIEWTRANSLATE)
		mode = "VIEW TRANSLATE";
	else if(m_mode == VIEWPERSPECTIVE)
		mode = "VIEW PERSPECTIVE";
	else if(m_mode == VIEWPORT)
		mode = "VIEWPORT";
	
	char buffer[50];
	std::string s = "Mode: " + mode + "\n";
	sprintf(buffer,"Near Plane: %.2f  Far Plane: %.2f",  p_near, p_far);
	m_label->set_label(s+buffer);
}

bool Viewer::on_button_press_event(GdkEventButton* event)
{
	switch(event->button){
        case 1:
            button1 = true;
            break;
        case 2:
            button2 = true;
            break;
        case 3:
            button3 = true;
            break;
        default:
            break;
    }
	init_point = Point2D(event->x, event->y);
	if(m_mode == VIEWPORT)
		init_viewport_point = Point2D(event->x, event->y);
  return true;
}

bool Viewer::on_button_release_event(GdkEventButton* event)
{
	switch(event->button){
        case 1:
            button1 = false;
            break;
        case 2:
            button2 = false;
            break;
        case 3:
            button3 = false;
            break;
        default:
            break;
    }
  return true;
}

bool Viewer::on_motion_notify_event(GdkEventMotion* event)
{
	if(!button1 && !button2 && !button3)
		return false; 
	int delta = event->x - init_point[0];
	double factor;
	int temp_x, temp_y;
	switch(m_mode){
		case MODELROTATE:
			if(button1){
				m_model_transrot = m_model_transrot * rotation(delta/2, 'x');
			}
			if(button2){
				m_model_transrot = m_model_transrot * rotation(delta/2, 'y');
			}
			if(button3){
				m_model_transrot = m_model_transrot * rotation(delta/2, 'z');
			}
			break;
		case MODELTRANSLATE:
			if(button1){
				m_model_transrot = m_model_transrot * translation(Vector3D(delta/80.0,0,0));
			}
			if(button2){
				m_model_transrot = m_model_transrot * translation(Vector3D(0,delta/80.0,0));
			}
			if(button3){
				m_model_transrot = m_model_transrot * translation(Vector3D(0,0,delta/80.0));
			}
			break;
		case MODELSCALE:
			factor = 1.0 + delta/100.0;		
			if(button1){
				m_model_scale = m_model_scale * scaling(Vector3D(factor,1.0,1.0));
			}
			if(button2){
				m_model_scale = m_model_scale * scaling(Vector3D(1.0,factor,1.0));
			}
			if(button3){
				m_model_scale = m_model_scale * scaling(Vector3D(1.0,1.0,factor));
			}
			break;
		case VIEWROTATE:
			if(button1){
				m_view = rotation(delta/2, 'x') * m_view;
			}
			if(button2){
				m_view = rotation(delta/2, 'y') * m_view;
			}
			if(button3){
				m_view = rotation(delta/2, 'z') * m_view;
			}
			break;
		case VIEWTRANSLATE:
			if(button1){
				m_view = translation(Vector3D(delta/300.0,0,0)) * m_view;
			}
			if(button2){
				m_view = translation(Vector3D(0,delta/300.0,0)) * m_view;
			}
			if(button3){
				m_view = translation(Vector3D(0,0,delta/300.0)) * m_view;
			}
			break;
		case VIEWPERSPECTIVE:
			if(button1){
				p_FOV += delta/2.0;
				if(p_FOV < 5.0)
					p_FOV = 5.0;
				else if(p_FOV > 160.0)
					p_FOV = 160.0;
			}
			if(button2){
				p_near += delta/80.0;
				if(p_near > p_far)
					p_near = p_far;
				if(p_near < 0)
					p_near = 0;
			}
			if(button3){
				p_far += delta/80.0;
				if(p_near > p_far)
					p_far = p_near;
			}
			set_perspective(p_FOV, 1.0, p_near, p_far);
			setLabelContext();
			break;
		case VIEWPORT:
			temp_x = event->x;
			temp_y = event->y;
			if(temp_x > get_width())
				temp_x = get_width();
			if(temp_y > get_height())
				temp_y = get_height();
			if(temp_x < 0)
				temp_x = 0;
			if(temp_y < 0)
				temp_y = 0;
			final_viewport_point = Point2D(temp_x, temp_y);
			if(button1){
				setViewportAttributes(init_viewport_point, final_viewport_point);
			}
			break;
		default:
			break;
	}
	invalidate();
	init_point = Point2D(event->x, event->y);
  return true;
}
